<?php

/**
 * This file is part of Switcher.
 * 
 * Switcher is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Switcher is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Switcher. If not, see <http://www.gnu.org/licenses/>.
 * 
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 */

namespace switcher\config\factories;

/**
 * The <b>XmlConfigFactory</b> class is used by the application to obtain a xml-declared configuration.
 * The configuration is validated against a schema, which ensures the  creation of a valid <b>Config</b>.
 * This is the recommended way to manage configuration.
 * 
 * <p>The <b>XmlConfigFactory</b> class ensures that it will create a valid <b>Config</b>. However, there
 * is no guarentee of a valid configuration to be a working configuration. It is not the role of the factory
 * to check if a declared class is of a right type, it will not check if a declared <b>Controller<b> is of a
 * subclass of <b>switcher\controllers\Controller</b>.</p>
 * 
 * @author Dri <switcher.framework@gmail.com>
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 * @package switcher\config\factories
 * @since 1.0.0
 * @see switcher\config\Config
 */
final class XmlConfigFactory
{
	/**
	 * Schema for XML validation of the configuration
	 * @var string
	 */
	const CONFIG_FILENAME = 'config.xsd';
	
	/**
	 * XML namespace of the configuration
	 * @var string
	 */
	const CONFIG_NAMESPACE = 'http://www.switcherframework.org/config';
	
	/**
	 * Hidden constructor
	 */
	private function __construct()
	{
	}
	
	/**
	 * <p>Build a <b>Config</b> instance from a XML declaration. It can be either a file
	 * name or raw XML. The input is validated against a XSD schema to ensure a valid
	 * configuration.
	 * 
	 * @param $xml The XML declaration
	 * @return \switcher\config\Config A valid configuration
	 * @throws \InvalidArgumentException If logger-factory is not a logger factory
	 * @see switcher\config\Config
	 * @var switcher\log\LoggerFactory
	 */
	public static function getConfig($xml)
	{
		$xmlConfig = self::loadXmlConfig($xml);
		
		\switcher\log\LoggerFactory::setLoggerFactory( self::getLoggerFactory($xmlConfig) );
		
		$config =  self::createConfig($xmlConfig);
		
		$config->defaultRenderer = (string) $xmlConfig->renderers->attributes()->default;
		$config->defaultController = (string) $xmlConfig->controllers->attributes()->default;
		$config->defaultView = (string) $xmlConfig->views->attributes()->default;
		
		if ($xmlConfig->filters->count() == 1)
		{
			$config->filters = self::getItems($xmlConfig->filters->filter);
		}
		
		$config->renderers = self::getItems($xmlConfig->renderers->renderer);
		$config->controllers = self::getItems($xmlConfig->controllers->controller);
		$config->views = self::getViews($xmlConfig->views->view);
		
		return $config;
	}
	
	/**
	 * Loads the XML configuration from either a file, or a string.
	 * 
	 * @param string $xml The XML declaration
	 * @return SimpleXMLElement The XML configuration
	 * @throws \InvalidArgumentException If no XML could be loaded
	 * @throws \InvalidArgumentException If XML declaration is not valid
	 */
	private static function loadXmlConfig($xml)
	{
		$domConfig = new \DOMDocument();
		
		// accepts either xml file or xml string
		is_file($xml) ? @$domConfig->load($xml) : @$domConfig->loadXML($xml);
		
		if ($domConfig->documentElement == null)
		{
			throw new \InvalidArgumentException('Config Not Found : ' . $xml);
		}
		
		if ( !self::validateConfig($domConfig) )
		{
			throw new \InvalidArgumentException('Config Not Valid : ' . $xml);
		}
		
		return simplexml_import_dom($domConfig);
	}
	
	/**
	 * Validates the XML against the configuration XSD schema.
	 * 
	 * @param DOMDocument $domConfig The XML declaration DOM document
	 * @return bool True if the config is valid
	 */
	private static function validateConfig(\DOMDocument $domConfig)
	{
		$schema = pathinfo(__FILE__, PATHINFO_DIRNAME) . DIRECTORY_SEPARATOR . self::CONFIG_FILENAME;
		return @$domConfig->schemaValidate($schema);
	}
	
	/**
	 * Creates a <b>Config</b> instance and maps all the declared navigation
	 * attributes to the matching properties.
	 * 
	 * @param \SimpleXMLElement $xmlConfig The XML configuration
	 * @return \switcher\config\Config the newly created configuration
	 */
	private static function createConfig(\SimpleXMLElement $xmlConfig)
	{
		$config = new \switcher\config\Config();
		$config->root = (string) @$xmlConfig->attributes()->root;
		
		return $config;
	}
	
	/**
	 * Creates an instance of the declared <b>LoggerFactory</b> or by default, an instance
	 * of <b>SilentLogger</b>.
	 * 
	 * @param \SimpleXMLElement $xmlConfig The XML configuration
	 * @return \switcher\log\LoggerFactory a logger factory
	 * @throws \InvalidArgumentException If logger-factory is not a logger factory
	 * @see switcher\log\LoggerFactory
	 */
	private static function getLoggerFactory(\SimpleXMLElement $xmlConfig)
	{
		$class = (string) $xmlConfig->attributes()->{'logger-factory'};
		
		if ($class == null)
		{
			return new \switcher\log\SilentLogger();
		}
		
		if ( !is_subclass_of($class, '\switcher\log\LoggerFactory') )
		{
			throw new \InvalidArgumentException('Not a LoggerFactory : ' . $class);
		}
		
		return new $class();
	}
	
	/**
	 * Creates the declaration of named or ordered items such as filters, controllers,
	 * renderers or any future items.
	 * 
	 * @param \SimpleXMLElement $elements The items nodes
	 * @return array The items configuration declaration
	 */
	private static function getItems(\SimpleXMLElement $elements)
	{
		$items = array();
		
		foreach ($elements as $element)
		{
			$item = \switcher\utils\ReflectionHelper::createProperties( $element->children() );
			
			if ( isset($element->attributes()->type) )
			{
				$item['@type'] = (string) $element->attributes()->type;
			}
			
			if ( isset($element->attributes()->name) )
			{
				$key = (string) $element->attributes()->name;
			}
			else
			{
				$key = count($items);
			}
			
			if ( isset($element->attributes()->{'default-action'}) )
			{
				$item['@default-action'] = (string) $element->attributes()->{'default-action'};
			}
			
			unset( $item['@attributes'] );
			
			$items[$key] = $item;
		}
		
		return $items;
	}
	
	/**
	 * Creates the declaration of view items.
	 * 
	 * @param \SimpleXMLElement $elements The views nodes
	 * @return array The views configuration declaration
	 */
	private static function getViews(\SimpleXMLElement $elements)
	{
		$views = array();
		
		foreach ($elements as $element)
		{
			$view = array();
			$name = (string) $element->attributes()->name;
			
			$view['methods'] = explode(' ', (string) $element->attributes()->methods);
			$view['ajax'] = (string) @$element->attributes()->ajax;
			
			$view['path'] = self::getViewPath($element);
			
			$view['renderer'] = self::getViewItem($element, 'renderer');
			$view['controller'] = self::getViewItem($element, 'controller');
			$view['action'] = (string) @$element->controller->attributes()->action;
			
			$view['get-params'] = @self::getViewHttpParams($element->{'get-params'});
			$view['post-params'] = @self::getViewHttpParams($element->{'post-params'});
			
			$views[$name] = $view;
		}
		
		return $views;
	}
	
	/**
	 * Get the declared path of a view.
	 * 
	 * @param SimpleXMLElement $element The view node
	 * @return string The view's path declaration
	 */
	private static function getViewPath(\SimpleXMLElement $element)
	{
		if (@$element->path == null)
		{
			return null;
		}
		
		$path = array();
		
		$path['value'] = (string) $element->path;
		
		if (@$element->path->attributes()->regexp != null)
		{
			$path['regexp'] = $element->path->attributes()->regexp == 'true';
		}
		
		return $path;
	}
	
	/**
	 * Get the declared item (controller or renderer) of a view.
	 * 
	 * @param SimpleXMLElement $element The view node
	 * @return mixed The view's item declaration
	 */
	private static function getViewItem(\SimpleXMLElement $element, $type)
	{
		$item = array();
		
		if (@$element->$type->attributes()->name != null)
		{
			$item['name'] = (string) $element->$type->attributes()->name;
			$item['properties'] = \switcher\utils\ReflectionHelper::createProperties($element->$type);
			
			unset( $item['properties']['@attributes'] );
		}
		
		return $item;
	}
	
	/**
	 * Get the declared HTTP params (GET or POST) of a view.
	 * 
	 * @param SimpleXMLElement $element The view node
	 * @return array The view's HTTP params declaration
	 */
	private static function getViewHttpParams($elements)
	{
		$params = array();
		
		foreach ($elements->children() as $element)
		{
			$name = (string) $element->attributes()->name;
			$optional = ((string) $element->attributes()->optional) == 'true';
			$params[$name] = $optional;
		}
		
		return $params;
	}
}

?>